iT邦幫忙

2023 iThome 鐵人賽

DAY 9
0
Software Development

深入淺出設計模式 - 使用 C++系列 第 9

[Day 09] 將方法的呼叫封裝起來 - 命令模式 ( Command Pattern )

  • 分享至 

  • xImage
  •  

命令模式 (Command Pattern)

命令模式 (Command Pattern):

  • 可將請求封裝成物件,讓使用者可以將請求、佇列或紀錄...等物件參數化,並支援可復原的操作

組成:

  • Command(命令): 定義命令的接口,通常只有一個 execute() 方法
  • ConcreteCommand(具體命令): 實現 Command 接口,並綁定一個接收者來觸發該接收者的動作。通常會持有接收者的實例參考
  • Invoker(調用者或發起者): 請求 Command 物件進行請求。這通常是 UI 按鈕或者某些觸發事件的處理者
  • Receiver(接收者): 知道如何執行一個與執行請求相關的操作。任何類都可能成為一個接收者,只要它可以實現命令要求的動作
  • Client(客戶端): 創建 ConcreteCommand 對象並設定它的接收者

適用時機:

  • 參數化物件: 可以將操作放入指令佇列中
  • 延遲操作的執行: 放入佇列中的命令可以在任何時間點被執行,可以在不同的時刻指定、排列和執行請求
  • 支持撤銷操作: 命令模式可以讓你保持操作的歷史記錄,因此可以支援撤銷和重試操作
  • 系統需要根據發出的命令進行相對應的操作,但它不知道命令應當如何執行或者誰是執行命令的具體接收者

經典範例 (餐廳)

// Command Interface
class Command {
public:
    virtual void execute() = 0;
};

// Receiver: 廚師
class Chef {
public:
    void cookBurger() {
        std::cout << "製作漢堡" << std::endl;
    }

    void cookPizza() {
        std::cout << "製作披薩" << std::endl;
    }

    void cookSalad() {
        std::cout << "製作沙拉" << std::endl;
    }
};

// ConcreteCommand: 漢堡
class BurgerCommand : public Command {
private:
    Chef* chef;

public:
    BurgerCommand(Chef* c) : chef(c) {}

    void execute() override {
        chef->cookBurger();
    }
};

// ConcreteCommand: 披薩
class PizzaCommand : public Command {
private:
    Chef* chef;

public:
    PizzaCommand(Chef* c) : chef(c) {}

    void execute() override {
        chef->cookPizza();
    }
};

// ConcreteCommand: 沙拉
class SaladCommand : public Command {
private:
    Chef* chef;

public:
    SaladCommand(Chef* c) : chef(c) {}

    void execute() override {
        chef->cookSalad();
    }
};

// Invoker: 服務員
class Waiter {
private:
    std::vector<Command*> orders;

public:
    void takeOrder(Command* order) {
        orders.push_back(order);
    }

    void placeOrders() {
        for (auto order: orders) {
            order->execute();
        }
        orders.clear();
    }
};

// Client
int main() {
    Chef chef;  // Receiver

    // Client 創建命令並設定它的接收者
    BurgerCommand burger(&chef);
    PizzaCommand pizza(&chef);
    SaladCommand salad(&chef);

    // 服務員收到顧客的命令
    Waiter waiter;  // Invoker
    waiter.takeOrder(&burger);
    waiter.takeOrder(&pizza);
    waiter.takeOrder(&salad);

    // 服務員將命令發送給廚師
    waiter.placeOrders();
}

衍生: Meta Command Pattern

  • 是 Command Pattern 的一種進階應用,主要概念是組合多個命令以形成一個複合命令
  • Macro Command 是一個具體的 Command,但其包含一系列的 Command 對象。當調用其 execute() 方法時,它會依序執行其中的所有命令

過程:

  1. 創建一個 MacroCommand 類,它實現 Command 接口
  2. MacroCommand 內部應有一個 Command 類型的列表或數組
  3. 在 MacroCommand 的 execute() 方法中,遍歷並執行列表中的每一個 Command
  4. 可能還需要提供方法來添加和移除 Command。

適用時機:

Macro 錄製: 用戶可以錄製一系列的動作,然後將這些動作保存為一個單一的 Macro Command,以後可以隨時重播這些動作
Batch 執行: 當需要執行一系列的命令時,如資料庫的批量操作或文件操作

  • 複合命令:當某一操作需要多個步驟完成,而這些步驟需要被封裝為一個整體的命令時

這種模式的優點是增強了系統的靈活性,使得命令的執行更加動態和組合性。但同時也可能帶來更多的複雜性,尤其是當命令間有相互依賴

[補充] No Command 技巧

NoCommand:

  • 是 Command Pattern 中的一個常見技巧,其主要目的是提供一個 Null Operation 以避免 nullptr 異常...等非預期操作。它可以確保 Invoker 始終有 Command 可以 Execute,即使那個命令什麼都不做
class NoCommand : public Command {
public:
    void execute() override {
        // 什麼都不做
    }
};

int main() {
    Chef chef;  // Receiver

    // Client 創建命令並設定它的接收者
    NoCommand noCmd;

    // 服務員收到顧客的命令
    Waiter waiter;  // Invoker
    waiter.takeOrder(&noCmd);  // 此命令不會產生任何行為

    // 服務員將命令發送給廚師
    waiter.placeOrders();
}

Reference

[1]. https://refactoring.guru/design-patterns/command
[2]. https://ithelp.ithome.com.tw/articles/10204425
[3]. https://notfalse.net/4/command-pattern


上一篇
[Day 08] 獨一無二的物件 - 單例模式 (Singleton Pattern)
下一篇
[Day 10] 物件導向轉接器 - 轉接器模式 (Adapter Pattern)
系列文
深入淺出設計模式 - 使用 C++37
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言